﻿using RockBus.CacheManager.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Runtime.Caching;

namespace RockBus.Caching
{
    public class CacheManager : ICacheManager
    {
        private MemoryCache Cache { get; set; }

        private readonly ConcurrentDictionary<string, ValueFactory> valueFactories = new ConcurrentDictionary<string, ValueFactory>();

        public CacheManager()
        {
            this.Cache = MemoryCache.Default;
        }

        public void Set(string key, object value)
        {
            this.Cache.Set(key, value, DateTimeOffset.MaxValue);
        }

        public void Set(string key, object value, int timeToLiveSecs)
        {
            this.Cache.Set(
                key,
                value,
                new DateTimeOffset(DateTime.UtcNow, TimeSpan.FromSeconds(timeToLiveSecs))
                );
        }

        public void Set<T>(string key, T value) where T : class
        {
            this.Cache.Set(key, value, DateTimeOffset.MaxValue);
        }

        public void Set<T>(string key, T value, int timeToLiveSecs) where T : class
        {
            this.Cache.Set(
                key,
                value,
                new DateTimeOffset(DateTime.UtcNow, TimeSpan.FromSeconds(timeToLiveSecs))
                );
        }

        public object Get(string key)
        {
            return this.Cache.Get(key);
        }

        public T Get<T>(string key) where T : class
        {
            object obj = this.Cache.Get(key);
            if (null == obj)
            {
                return null;
            }

            T item = obj as T;
            if (null == item)
            {
                throw new InvalidCastException(
                    string.Format("Unable to cast object with key {0} to type {1}",
                    key,
                    typeof(T).Name));
            }

            return item;
        }

        public object Remove(string key)
        {
            object item = this.Get(key);
            this.Cache.Remove(key);
            return item;
        }

        public T Remove<T>(string key) where T : class
        {
            T item = this.Get<T>(key);
            this.Cache.Remove(key);
            return item;
        }

        public void AutoAdd<T>(string key, Func<T> valueFactory, bool isThreadSafe) where T : class
        {
            this.valueFactories.TryAdd(key, new ValueFactory<T>(valueFactory, isThreadSafe));
        }

        public void AutoAdd<T>(string key, Func<T> valueFactory, bool isThreadSafe, int timeToLiveSecs) where T : class
        {
            this.valueFactories.TryAdd(key, new ValueFactory<T>(valueFactory, isThreadSafe, timeToLiveSecs));
        }

        public object AutoGet(string key)
        {
            var value = this.Cache.Get(key);
            if (null != value)
            {
                return value;
            }

            ValueFactory valueFactory = null;
            if (!this.valueFactories.TryGetValue(key, out valueFactory))
            {
                // If we don't have a valueFactory, we can't produce the value.
                return null;
            }

            if (valueFactory.IsThreadSafe)
            {
                lock (valueFactory.LockObject)
                {
                    // Unfortunate, but necessary, to check again for presence in the cache from within a lock
                    value = Cache.Get(key);
                    if (null != value)
                    {
                        return value;
                    }

                    value = valueFactory.Func();
                    this.Set(key, value);
                    return value;
                }
            }

            value = valueFactory.Func();
            this.Set(key, value);
            return value;
        }

        public T AutoGet<T>(string key) where T : class
        {
            var obj = this.AutoGet(key);
            if (null == obj)
            {
                return null;
            }

            T item = obj as T;
            if (null == item)
            {
                throw new InvalidCastException(
                    string.Format("Unable to cast object with key {0} to type {1}",
                    key,
                    typeof(T).Name));
            }

            return item;
        }

        public object AutoRemove(string key)
        {
            object item = this.Get(key);

            ValueFactory valueFactory = null;
            this.valueFactories.TryRemove(key, out valueFactory);

            this.Cache.Remove(key);
            return item;
        }

        public T AutoRemove<T>(string key) where T : class
        {
            var obj = this.AutoRemove(key);
            if (null == obj)
            {
                return null;
            }

            T item = obj as T;
            if (null == item)
            {
                throw new InvalidCastException(
                    string.Format("Unable to cast object with key {0} to type {1}",
                    key,
                    typeof(T).Name));
            }

            return item;
        }
    }
}